#include "StdAfx.h"

#include "Config.h"
#include "CryLobby.h"
#include "CryHostMigration.h"
#include "CryMatchMaking.h"

#include "IStatoscope.h"

#if NETWORK_HOST_MIGRATION

static const float HOST_MIGRATION_LOG_INTERVAL = 1.0f;

void CHostMigration::Start(SHostMigrationInfo_Private* pInfo)
{
	if (gEnv->pStatoscope)
	{
		gEnv->pStatoscope->AddUserMarker("Host Migration Start");
	}

	pInfo->m_startTime = g_time;
	pInfo->m_logTime.SetSeconds(pInfo->m_startTime.GetSeconds() - (HOST_MIGRATION_LOG_INTERVAL * 2.0f));

	ResetListeners(pInfo);

	// Indicate we're performing host migration
	pInfo->m_state = eHMS_Initiate;
	gEnv->bHostMigrating = true;

	// Need to do this here so that initiate gets called before the GameClientChannel is destroyed
	SCOPED_GLOBAL_LOCK;
	TO_GAME_FROM_LOBBY(&CHostMigration::Update, this, pInfo);
}

void CHostMigration::Update(SHostMigrationInfo_Private* pInfo)
{
	// We can get here with an idle state if the main thread stalls for too long since these come from the network thread
	if (pInfo->m_state == eHMS_Idle)
	{
		return;
	}

	bool done = true;

	while (done) 
	{
		THostMigrationEventListenerVector::iterator it;
		THostMigrationEventListenerVector &listeners = CNetwork::Get()->GetHostMigrationListeners();

		CCryMatchMaking* pMatchMaking = static_cast<CCryMatchMaking*>(CCryLobby::GetLobby()->GetMatchMaking());
		CrySessionHandle sessionIndex = pMatchMaking->GetSessionHandleFromGameSessionHandle(pInfo->m_session);
		CRY_ASSERT(sessionIndex != CrySessionInvalidHandle);

		float timeout = CNetCVars::Get().hostMigrationTimeout;
		float timetaken = (g_time - pInfo->m_startTime).GetSeconds();

		// Log each state once a second to reduce spam
		pInfo->m_logProgress = false;
		if ((g_time - pInfo->m_logTime).GetSeconds() > HOST_MIGRATION_LOG_INTERVAL)
		{
			switch (pInfo->m_state)
			{
			case eHMS_Initiate:
				NetLog("[Host Migration]: Initiate (session %i) [timer %fs, timeout is %fs]", sessionIndex, timetaken, timeout);
				break;
			case eHMS_DisconnectClient:
				NetLog("[Host Migration]: Disconnect client (session %i) [timer %fs, timeout is %fs]", sessionIndex, timetaken, timeout);
				break;
			case eHMS_DemoteToClient:
				NetLog("[Host Migration]: Demote to client (session %i) [timer %fs, timeout is %fs]", sessionIndex, timetaken, timeout);
				break;
			case eHMS_PromoteToServer:
				NetLog("[Host Migration]: Promote to server (session %i) [timer %fs, timeout is %fs]", sessionIndex, timetaken, timeout);
				break;
			case eHMS_WaitForNewServer:
				NetLog("[Host Migration]: Waiting for server identification (session %i) [timer %fs, timeout is %fs]", sessionIndex, timetaken, timeout);
				break;
			case eHMS_ReconnectClient:
				NetLog("[Host Migration]: Reconnect client (session %i) [timer %fs, timeout is %fs]", sessionIndex, timetaken, timeout);
				break;
			case eHMS_Finalise:
				NetLog("[Host Migration]: Finalise (session %i) [timer %fs, timeout is %fs]", sessionIndex, timetaken, timeout);
				break;
			case eHMS_Terminate:
				NetLog("[Host Migration]: Terminate (session %i)", sessionIndex);
				break;
			case eHMS_Resetting:
				NetLog("[Host Migration]: Resetting");
				break;
			}

			pInfo->m_logProgress = true;
			pInfo->m_logTime = g_time;
		}

		for (it = listeners.begin(); it != listeners.end(); ++it)
		{
			if (!it->m_done[sessionIndex])
			{
				switch (pInfo->m_state)
				{
				case eHMS_Initiate:
					it->m_done[sessionIndex] = it->m_pListener->OnInitiate(*pInfo, it->m_state[sessionIndex]);
					if (pInfo->m_state == eHMS_Terminate)
					{
						return;
					}
					break;
				case eHMS_DisconnectClient:
					it->m_done[sessionIndex] = it->m_pListener->OnDisconnectClient(*pInfo, it->m_state[sessionIndex]);
					if (pInfo->m_state == eHMS_Terminate)
					{
						return;
					}
					break;
				case eHMS_WaitForNewServer:
					{
						it->m_done[sessionIndex] = false;

						pMatchMaking->HostMigrationWaitForNewServer(sessionIndex);
						if (pInfo->m_state == eHMS_Terminate)
						{
							return;
						}

						char address[HOST_MIGRATION_MAX_SERVER_NAME_SIZE];
						strncpy(address, pInfo->m_newServer.c_str(), HOST_MIGRATION_MAX_SERVER_NAME_SIZE);
						address[HOST_MIGRATION_MAX_SERVER_NAME_SIZE - 1] = 0; // ensure null-termination

						if (pMatchMaking->GetNewHostAddress(address, pInfo))
						{
							pInfo->SetNewServer(address);
							pInfo->m_state = pInfo->IsNewHost() ? eHMS_PromoteToServer : eHMS_DemoteToClient;
							return;
						}
					}
					break;
				case eHMS_DemoteToClient:
					it->m_done[sessionIndex] = (!pInfo->IsNewHost()) ? it->m_pListener->OnDemoteToClient(*pInfo, it->m_state[sessionIndex]) : true;
					if (pInfo->m_state == eHMS_Terminate)
					{
						return;
					}
					break;
				case eHMS_PromoteToServer:
					it->m_done[sessionIndex] = (pInfo->IsNewHost()) ? it->m_pListener->OnPromoteToServer(*pInfo, it->m_state[sessionIndex]) : true;
					if (pInfo->m_state == eHMS_Terminate)
					{
						return;
					}
					break;
				case eHMS_ReconnectClient:
					it->m_done[sessionIndex] = it->m_pListener->OnReconnectClient(*pInfo, it->m_state[sessionIndex]);
					if (pInfo->m_state == eHMS_Terminate)
					{
						return;
					}
					break;
				case eHMS_Finalise:
					it->m_done[sessionIndex] = it->m_pListener->OnFinalise(*pInfo, it->m_state[sessionIndex]);
					if (pInfo->m_state == eHMS_Terminate)
					{
						return;
					}
					break;
#if ENABLE_HOST_MIGRATION_STATE_CHECK
				case eHMS_StateCheck:
					it->m_done[sessionIndex] = pInfo->m_stateCheckDone;
					break;
#endif
				case eHMS_Terminate:
					it->m_done[sessionIndex] = it->m_pListener->OnTerminate(*pInfo, it->m_state[sessionIndex]);
					break;
				case eHMS_Resetting:
					it->m_done[sessionIndex] = it->m_pListener->OnReset(*pInfo, it->m_state[sessionIndex]);
					if (pInfo->m_state == eHMS_Terminate)
					{
						return;
					}
					break;
				}
			}

			done &= it->m_done[sessionIndex];
		}

		if (pInfo->m_state == eHMS_Finalise)
		{
			// Hold in OnFinalise until matchmaking side of host migration is finished
			done &= pInfo->m_matchMakingFinished;
		}

		if (done)
		{
			switch (pInfo->m_state)
			{
			case eHMS_Terminate:
				// Emergency disconnect
				//gEnv->pConsole->ExecuteString("wait_frames 1");
				//gEnv->pConsole->ExecuteString("disconnect", false, true);
				NetLog("[Host Migration]: terminated (session %i)", sessionIndex);
				// Intentional fall-through
			case eHMS_Finalise:
#if ENABLE_HOST_MIGRATION_STATE_CHECK
			case eHMS_StateCheck:
				if ((pInfo->m_state == eHMS_Finalise) && CNetCVars::Get().doHostMigrationStateCheck)
				{
					pInfo->m_state = eHMS_StateCheck;
					pMatchMaking->HostMigrationStateCheck(sessionIndex);
				}
				else
#endif
				{
					// We're done
					pInfo->m_state = eHMS_Idle;
					gEnv->bHostMigrating = static_cast<CCryMatchMaking*>(CCryLobby::GetLobby()->GetMatchMaking())->AreAnySessionsMigrating();
					pMatchMaking->HostMigrationFinalise(sessionIndex);
					NetLog("[Host Migration]: complete (session %i) [timer %fs, timeout is %fs]", sessionIndex, timetaken, timeout);

					if (pInfo->m_storedContextState)
					{
						pInfo->m_storedContextState->Die();
						pInfo->m_storedContextState = NULL;
					}

					if (gEnv->pStatoscope)
					{
						gEnv->pStatoscope->AddUserMarker("Host Migration Finish");
					}

					done = false;
				}

				break;

			default:
				// Move to the next state
				for (it = listeners.begin(); it != listeners.end(); ++it)
				{
					it->Reset(sessionIndex);
				}

				// Ensure logging of state will occur with this state change
				pInfo->m_logTime.SetSeconds(g_time.GetSeconds() - (HOST_MIGRATION_LOG_INTERVAL * 2.0f));

				if (pInfo->m_state == eHMS_Resetting)
				{
					pInfo->m_state = eHMS_DisconnectClient;
				}
				else
				{
					pInfo->m_state = (eHostMigrationState)((uint32)(pInfo->m_state) + 1);
				}
				break;
			}
		}
		else
		{
			if (timetaken > timeout)
			{
				NetLog("[Host Migration]: Timed out - terminating");
				Terminate(pInfo);
			}
		}
	}
}

void CHostMigration::Terminate(SHostMigrationInfo_Private* pInfo)
{
#if HOST_MIGRATION_SOAK_TEST_BREAK_DETECTION
	ICVar* pVar = gEnv->pConsole->GetCVar("gl_debugLobbyHMTerminations");
	if (pVar != NULL)
	{
		pVar->Set(pVar->GetIVal() + 1);
	}
#endif

	if (pInfo->m_state != eHMS_Idle)
	{
		ResetListeners(pInfo);
		pInfo->m_state = eHMS_Terminate;
	}
}

void CHostMigration::Reset(SHostMigrationInfo_Private* pInfo)
{
	ResetListeners(pInfo);
	pInfo->m_state = eHMS_Resetting;
}

void CHostMigration::ResetListeners(SHostMigrationInfo_Private* pInfo)
{
	THostMigrationEventListenerVector &listeners = CNetwork::Get()->GetHostMigrationListeners();
	CCryMatchMaking* pMatchMaking = static_cast<CCryMatchMaking*>(CCryLobby::GetLobby()->GetMatchMaking());
	CrySessionHandle sessionIndex = pMatchMaking->GetSessionHandleFromGameSessionHandle(pInfo->m_session);
	CRY_ASSERT(sessionIndex != CrySessionInvalidHandle);

	for (THostMigrationEventListenerVector::iterator it = listeners.begin(); it != listeners.end(); ++it)
	{
		it->Reset(sessionIndex);
	}
}
#endif